IDEs & Debugging

Debugging tests

The ability to run your scripts through a debugger increases the speed and flexibility with which you can discern the behaviour of code in your software. Alongside this, by using tests you can easily run small sections of your code under controlled conditions. Combining these together gives you a great way to work out why parts of a larger program are perhaps not working as you expect.

The process for debugging your tests is almost exactly the same as for debugging scripts. Breakpoints and stepping, for example, work in the same way.

Let's work through an example to see what differences there are. Start by making a new file called lists.py with the following contents:

lists.py
def double_list(l):
    new_list = l
    for i in range(len(new_list)):
        new_list[i] *= 2
    return new_list

Then make a second file, test_lists.py to hold the tests:

test_lists.py
from lists import double_list


def test_double_list():
    my_list = [1, 2, 3]
    doubled_list = double_list(my_list)

    assert doubled_list == [2, 4, 6]


def test_double_list_relative():
    my_list = [1, 2, 3]
    doubled_list = double_list(my_list)

    assert my_list[0] * 2 == doubled_list[0]

From a quick glance at the code, it looks like the tests should probably pass but let's run them to check.

Run all the tests and you should see that all the tests pass except test_double_list_relative. This test is calling the double_list function and then comparing the original list passed in to the returned list to check that it was doubled correctly.

Let's use the debugger to work out what's going wrong.

Running tests through the debugger

The process for running a test in debug mode is very similar to running a test normally.

Let's start by placing a breakpoint at a location that we think will be useful. Since it's likely that there's something wrong with the code inside double_list, let's put the breakpoint in the test function, just at the point where that function is called. Put the breakpoint on line 13 of test_lists.py (doubled_list = double_list(my_list)).

There will usually be a button or option to start the debugger, near to that which you used for running a single test.

In PyCharm, when clicking the green arrow next to the test, there's a second option to Debug 'pytest for test_list...':

PyCharm debug one test

In VS Code, next to the Run Test button, there's a Debug Test button:

VS Code debug one test

Start the test in the debugger and it should pause execution on line 13 of test_lists.py. The only variable defined at this point should be my_list with the value [1, 2, 3].

Since we've paused on a line with a function call, we can step into the function so go ahead and do that.

You'll now be sitting on line 2 of lists.py with the variable l (the function parameter) set to [1, 2, 3]. This is same value as my_list which makes sense as we passed it as an the argument to this function.

Step over to the next line so that you're paused on line 3. Now we see both l and new_list shown in the variable list.

Step over once more to get to line 4. Now you'll see i is also set. This variable is counting over the indices of new_list so that each element can be updated one at a time. The line that we're paused on is going to update the 0th element of the list new_list to be double its current value. We expect that if we step over then the value of new_list in the variable list should update before our eyes.

PyCharm debug start loop

VS Code debug start loop

While paying attention to the value of new_list, step over to the next line of code. You'll now be on line 3 again since we're in a for loop. The first element of new_list has indeed been changed from 1 to 2 so the doubling seems to be working. However, look at the value of l. This was our input argument for the function but it's been changed too!

PyCharm debug in loop

VS Code debug in loop

This time keeping an eye on l in the variable list, step over twice to get back to line 3. You'll see that once more both l and new_list are changed. Clearly there's something causing the two variables to be linked together.

Place a breakpoint on line 5 of lists.py and press the Resume/Continue button to jump out of the loop. Now, looking at the values of new_list and l you'll see that they're both equal to [2, 4, 6].

PyCharm debug in loop

VS Code debug in loop

Step over or step out to get to line 15 of test_lists.py. We're now sitting at the point where the assert happens. Looking at the values of my_list and doubled_list we can see that if you take the 0th element of my_list and multiply it by two, it will not match the 0th element of doubled_list so the assert will indeed fail as the failing test shows.

Unlike when we were running our simple script through the debugger, our tests are being run for us by pytest. At this point if you keep on stepping over or stepping out will will end up in code from pytest. Now we've finished investigating what's going on with the variable and seen that the two lists inside double_list are incorrectly linked we can stop the debugger by pressing the red square stop button.

Fixing the problem

Now we understand the mechanics of the function a little better, we can start fixing it. A debugger doesn't magically tell us the answer, it is simply a tool to provide us with information.

In Python when you edit one variable and it changes another, it is usually caused by a mistake in copying a variable. Python's = doesn't copy the data on the right-hand side, it creates a new name for the same data. So in double_list the variables l and new_list are pointing at exactly the same data behind the scenes. This is why changing one changes the other.

The second cause is that when we passed my_list to the function, internally it is referring to it as l. In some programming languages it would have taken a copy of my_list for the function's use but in Python the variables inside refer to the same data as outside. This means than when l is changed, so is my_list.

Therefore, by line 3 of lists.py, we have three names for the same piece of data: my_list, l and new_list and changing any one of them will change the rest.

Exercise

Fix the double_list function so that both the tests pass.

answer